/*
 * Copyright 2011 Stefan Ferstl
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.code.debmavenplugin.archiver;

import java.io.File;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This class describes the content of an Archive. The archive is described by a
 * base directory whose files and subdirectories will be stored in the archive.
 * Files/Directories with special settings (aka special files) and exclusions
 * (files that should not be part of the archive) may be defined additionally.
 * The archive itself may be configured with default directory and file modes
 * according to the UNIX standard and with a default user and a default group.
 * All directories and files within the archive will be configured with the
 * defined permissions, user and group. The archive's data is represented as
 * objects of
 * {@link com.google.code.debmavenplugin.archiver.ArchiveMetaData.ArchiveEntryMetaData}
 * .
 */
public class ArchiveMetaData {
  private final Logger logger = LoggerFactory.getLogger(getClass());

  private ArchiveEntryMetaData defaultDirectorySettings;
  private ArchiveEntryMetaData defaultFileSettings;
  private Set<File> exclusions;
  private Map<File, ArchiveEntryMetaData> specialFiles;

  /**
   * Constructor.
   * @param baseDirectory The base directory of the archive's content. Must not
   *        be <code>null</code>.
   * @param userName Default user who ownes all the directories and files in the
   *        archive.
   * @param groupName Default group who ownes all the directories and files in
   *        the archive.
   * @param dirMode Default directory mode.
   * @param fileMode Default file mode.
   */
  public ArchiveMetaData(File baseDirectory, String userName, String groupName, Integer dirMode, Integer fileMode) {
    if (baseDirectory == null) {
      throw new IllegalArgumentException("Base directory must not be null");
    }

    this.defaultDirectorySettings = new ArchiveEntryMetaData(baseDirectory, userName, groupName, dirMode);
    this.defaultFileSettings = new ArchiveEntryMetaData(baseDirectory, userName, groupName, fileMode);

    this.exclusions = new HashSet<File>();
    this.specialFiles = new LinkedHashMap<File, ArchiveMetaData.ArchiveEntryMetaData>();
  }

  /**
   * Adds a special file to this metadata.
   * @param relativePath Path to the directory or file relative to the base
   *        directory (must not be <code>null</code>).
   * @param userName User that owns the special file.
   * @param groupName Group that owns the special file.
   * @param fileMode UNIX file mode.
   * @throws IllegalArgumentException In case the relative path is
   *         <code>null</code>.
   */
  public void addSpecialFile(String relativePath, String userName, String groupName, Integer fileMode) {
    if (relativePath == null) {
      throw new IllegalArgumentException("Relative path must not be null");
    }
    File specialFile = new File(getBaseDirectory(), relativePath);
    if (this.specialFiles.containsKey(specialFile)) {
      // WARN if duplicate (include the duplicated file settings)
      this.logger.warn("Special file '{}' has already been defined", specialFile);
    }
    ArchiveEntryMetaData metaDataEntry = new ArchiveEntryMetaData(specialFile, userName, groupName, fileMode);
    this.specialFiles.put(specialFile, metaDataEntry);
  }

  /**
   * Adds a file or directory exclusion to this metadata.
   * @param relativePath Path to the directory or file relative to the base
   *        directory.
   */
  public void addExclusion(String relativePath) {
    File excludedFile = new File(getBaseDirectory(), relativePath);
    if (this.exclusions.contains(excludedFile)) {
      // WARN if duplicate
      this.logger.warn("File '{}' has already been excluded.", excludedFile);
    }
    this.exclusions.add(excludedFile);
  }

  /**
   * Returns the defaultDirectorySettings.
   * @return The defaultDirectorySettings.
   */
  public ArchiveEntryMetaData getDefaultDirectorySettings() {
    return this.defaultDirectorySettings;
  }


  /**
   * Returns the defaultFileSettings.
   * @return The defaultFileSettings.
   */
  public ArchiveEntryMetaData getDefaultFileSettings() {
    return this.defaultFileSettings;
  }


  /**
   * Returns the meta data of the given file (must not be <code>null</code>).
   * @return The meta data of the given file.
   */
  public ArchiveEntryMetaData getEntryMetaData(File file) {
    if (file == null) {
      throw new IllegalArgumentException("File must not be null");
    }
    ArchiveEntryMetaData entryMetaData = this.specialFiles.get(file);
    if (entryMetaData == null) {
      entryMetaData = createEntryMetaData(file);
    }
    return entryMetaData;
  }

  /**
   * Returns the base directory.
   * @return The base directory.
   */
  public File getBaseDirectory() {
    return getDefaultDirectorySettings().getFile();
  }

  /**
   * Checks if the given file is excluded. If the given file is located in an
   * excluded directory, it is excluded as well.
   * @param file The file to check.
   * @return <code>true</code> if the file is excluded, <code>false</code> else.
   */
  public boolean isExcluded(File file) {
    boolean excluded = false;

    // Traverse up to the base directory and check if
    // a parent directory is excluded.
    File p = file;
    while (p != null || getBaseDirectory().equals(file)) {
      if (this.exclusions.contains(p)) {
        excluded = true;
        break;
      }
      p = p.getParentFile();
    }

    return excluded;
  }

  /**
   * Checks if the given file is a special file.
   * @return <code>true</code> in case of a special file, <code>false</code>
   *         else.
   */
  protected boolean isSpecialFile(File file) {
    return this.specialFiles.containsKey(file);
  }


  /**
   * Creates the metadata for the given file.
   * @param file The file for which the metatdata should be created.
   * @return The created metadata.
   */
  private ArchiveEntryMetaData createEntryMetaData(File file) {
    ArchiveEntryMetaData defaultSettings =
      file.isDirectory() ? getDefaultDirectorySettings() : getDefaultFileSettings();
    ArchiveEntryMetaData entryMetaData = new ArchiveEntryMetaData(
        file, defaultSettings.getUserName(), defaultSettings.getGroupName(), defaultSettings.getFileMode());
    return entryMetaData;
  }


  /**
   * Metadata of a directory or file in the archive.
   */
  public class ArchiveEntryMetaData {
    File file;
    private String userName;
    private String groupName;
    private Integer fileMode;

    /**
     * Constructor.
     * @param file The file represented by this metadata.
     * @param userName The user who owns the file.
     * @param groupName The group who owns the file.
     * @param fileMode UNIX file mode.
     */
    public ArchiveEntryMetaData(File file, String userName, String groupName, Integer fileMode) {
      if (file == null) {
        throw new IllegalArgumentException("File metadata must not be null");
      }
      this.file = file;
      this.userName = userName;
      this.groupName = groupName;
      this.fileMode = fileMode;
    }

    /**
     * Constructor using default user group and file mode.
     * @param file The file represented by this metadata.
     */
    public ArchiveEntryMetaData(File file) {
      this(file, null, null, null);
    }

    /**
     * Returns the file.
     * @return The file.
     */
    public File getFile() {
      return this.file;
    }


    /**
     * Returns the userName.
     * @return The userName.
     */
    public String getUserName() {
      return this.userName;
    }


    /**
     * Returns the groupName.
     * @return The groupName.
     */
    public String getGroupName() {
      return this.groupName;
    }


    /**
     * Returns the fileMode.
     * @return The fileMode.
     */
    public Integer getFileMode() {
      return this.fileMode;
    }

    /**
     * Checks if this metadata represents a directory..
     * @return <code>true</code> in case of a directory.
     */
    public boolean isDirectory() {
      return this.file.isDirectory();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean equals(Object obj) {
      return this.file.equals(obj);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int hashCode() {
      return this.file.hashCode();
    }
  }

}
